﻿using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DonetArxLib.CuveProjection
{
    internal class CurveMarker
    {
        public static string DIM_LAYER = "DimLayer";
        public static string TEMP_LAYER = "TempLayer";
        public static string ERROR_LAYER = "ErrorLayer";

        List<IndexRange> _straightLineRanges = new List<IndexRange>();
        List<IndexRange> _arcLineIndex = new List<IndexRange>();

        private List<Point3d> _points = new List<Point3d>();
        private ObjectId _dimId = ObjectId.Null;

        public CurveMarker()
        {
            var doc = Application.DocumentManager.MdiActiveDocument;
            UtilFunc.EnsureLayerExist(doc.Database, DIM_LAYER, 2);
            UtilFunc.EnsureLayerExist(doc.Database, TEMP_LAYER, 3);
            UtilFunc.EnsureLayerExist(doc.Database, ERROR_LAYER, 1);


            var trans = UtilFunc.GetTopTransation();

            var dimTable = trans.GetObject(doc.Database.DimStyleTableId, OpenMode.ForRead) as DimStyleTable;
            foreach (var id in dimTable)
            {
                _dimId = id;
                break;
            }
        }

        public void Mark(Polyline pl, double tol = 3.0)
        {
            if (pl == null)
                throw new ArgumentNullException(nameof(pl));

            CurveFitConfig.MaxTol = tol;

            _points = GetPointsFromPolyline(pl);
            if (_points.Count < 2)
                throw new InvalidOperationException("pl vertex count < 2");
            else if (_points.Count == 2)
                AddToStraightLine(0, 1);
            else
                TryArcFit();

            CreateResultDim();
        }

        public void Mark(Polyline2d pl, double tol = 3.0)
        {
            if (pl == null)
                throw new ArgumentNullException(nameof(pl));

            CurveFitConfig.MaxTol = tol;

            _points = GetPointsFromPolyline(pl);
            if (_points.Count < 3)
                throw new InvalidOperationException("pl vertex count < 3");
            else
                TryArcFit();

            CreateResultDim();
        }

        private void CreateResultDim()
        {
            foreach (var rg in _straightLineRanges)
            {
                UtilFunc.AddLineToCurrentSpace(_points[rg.Start], _points[rg.End], TEMP_LAYER);

                var pt1 = _points[rg.Start];
                var pt2 = _points[rg.End];
                var mid = new Point3d((pt1.X + pt2.X) / 2, (pt1.Y + pt2.Y) / 2, pt1.Z);
                var ptDim = mid + Vector3d.XAxis * 700;

                AlignedDimension lineDim = new AlignedDimension(_points[rg.Start], _points[rg.End], ptDim, null, _dimId);
                lineDim.Dimtxt = 100;
                lineDim.Dimasz = 50;
                lineDim.Dimtix = true;
                UtilFunc.AddToCurrentSpace(lineDim, UtilFunc.GetTopTransation(), DIM_LAYER);
            }

            foreach (var rg in _arcLineIndex)
            {
                var layerName = rg.ArxFitTolerance > CurveFitConfig.MaxTol ? ERROR_LAYER : DIM_LAYER;

                var arc = CreateArc(rg.Start, rg.End);
                arc.SetDatabaseDefaults();
                UtilFunc.AddToCurrentSpace(arc, UtilFunc.GetTopTransation(), TEMP_LAYER);

                var midDist = (arc.GetDistAtPoint(arc.StartPoint) + arc.GetDistAtPoint(arc.EndPoint)) / 2.0;
                var midPoint = arc.GetPointAtDist(midDist);

                var vec = (midPoint - arc.Center).GetNormal();
                var arcDimPoint = midPoint + vec * 700;
                var radDimPoint = midPoint - vec * 700;

                ArcDimension arcDim = new ArcDimension(arc.Center, arc.StartPoint, arc.EndPoint, arcDimPoint, null, _dimId);
                arcDim.Dimtxt = 100;
                arcDim.Dimasz = 50;
                arcDim.Dimtix = true;
                UtilFunc.AddToCurrentSpace(arcDim, UtilFunc.GetTopTransation(), layerName);

                RadialDimension radDim = new RadialDimension(arc.Center, midPoint, -200, null, _dimId);
                radDim.Dimtxt = 100;
                radDim.Dimasz = 50;
                UtilFunc.AddToCurrentSpace(radDim, UtilFunc.GetTopTransation(), layerName);

                if (layerName == ERROR_LAYER)
                {
                    var num = rg.ArxFitTolerance.ToString("F2");

                    DBText text = new DBText();
                    text.SetDatabaseDefaults();
                    text.TextString = $"tol={num}";
                    text.Position = midPoint - Vector3d.YAxis * 170;
                    text.Height = 100;
                    UtilFunc.AddToCurrentSpace(text, UtilFunc.GetTopTransation(), layerName);
                }
            }
        }

        private Arc CreateArc(int start, int end)
        {
            var mid = (start + end) / 2;

            CircularArc2d arc = new CircularArc2d(_points[start].ToPoint2d(), _points[mid].ToPoint2d(), _points[end].ToPoint2d());

            var center = arc.Center;
            var radius = arc.Radius;

            var vecStart = _points[start].ToPoint2d() - center;
            var vecEnd = _points[end].ToPoint2d() - center;

            var startAng = vecStart.Angle;
            var endAng = vecEnd.Angle;

            return new Arc(center.ToPoint3d(_points[0].Z), Vector3d.ZAxis, radius, startAng, endAng);
        }

        private bool TryArcFit()
        {
            int startIndex = 0;
            int curIndex = 2;

            while (true)
            {
                if (startIndex >= _points.Count || curIndex >= _points.Count)
                    break;

                if (curIndex - startIndex < 2)
                {
                    if (curIndex == _points.Count - 1)
                        TryFitLastArc(startIndex, curIndex);

                    curIndex++;
                }
                else
                {
                    var fr = TryArcFit(startIndex, curIndex);
                    if (fr.IsArcFit)
                    {
                        if (fr.MaxFitTolerance < CurveFitConfig.MaxTol)
                        {
                            if (AddToArcLine(startIndex, curIndex, fr.Arc, fr.MaxFitTolerance))
                                curIndex++;
                            else
                                throw new InvalidOperationException("invalid fit");
                        }
                        else if (curIndex - startIndex == 2)
                        {
                            if (AddToArcLine(startIndex, curIndex, fr.Arc, fr.MaxFitTolerance))
                                curIndex++;
                            else
                                throw new InvalidOperationException("invalid fit");
                        }
                        else
                            startIndex = curIndex - 1;
                    }
                    else
                        throw new InvalidOperationException("invalid oper!");
                }
            }

            return false;
        }

        private bool TryFitLastArc(int start, int end)
        {
            if (_arcLineIndex.Count < 1)
                return false;

            if (_arcLineIndex.Last().End - _arcLineIndex.Last().Start <= 3)
                return false;

            if (_arcLineIndex.Last().End != start)
                return false;

            _arcLineIndex.Last().End -= 1;
            start -= 1;

            var res = TryArcFit(start, end);
            if (!res.IsArcFit || res.MaxFitTolerance > CurveFitConfig.MaxTol)
                throw new InvalidOperationException("last arc fit error!");

            AddToArcLine(start, end, res.Arc, res.MaxFitTolerance);

            return true;
        }

        private bool AddToArcLine(int startIndex, int endIndex, CircularArc3d arc, double tol)
        {
            if (_arcLineIndex.Count < 1)
            {
                _arcLineIndex.Add(new IndexRange(startIndex, endIndex) { arc = arc, ArxFitTolerance = tol });
                return true;
            }

            if (_arcLineIndex.Last().Start == startIndex && _arcLineIndex.Last().End < endIndex)
            {
                _arcLineIndex.Last().End = endIndex;
                _arcLineIndex.Last().arc = arc;
                _arcLineIndex.Last().ArxFitTolerance = tol;
                return true;
            }

            if (startIndex >= _arcLineIndex.Last().End)
            {
                _arcLineIndex.Add(new IndexRange(startIndex, endIndex) { arc = arc, ArxFitTolerance = tol });
                return true;
            }

            return false;
        }

        private bool AddToStraightLine(int startIndex, int endIndex)
        {
            // 如果当前没有内容，且第一个长度满足独立直线段长度，加入直线段
            if (_straightLineRanges.Count == 0
                && _points[startIndex].DistanceTo(_points[endIndex]) >= CurveFitConfig.MinSegmentLengthTreatAsLine)
            {
                _straightLineRanges.Add(new IndexRange(startIndex, endIndex));
                return true;
            }

            // 如果新加入的直线段和之前的不连续，且长度满足独立直线段长度，加入直线段
            if (_straightLineRanges.Last().End != startIndex
                && _points[startIndex].DistanceTo(_points[endIndex]) >= CurveFitConfig.MinSegmentLengthTreatAsLine
                && endIndex - startIndex == 1)
            {
                _straightLineRanges.Add(new IndexRange(startIndex, endIndex));
                return true;
            }

            // 如果新加入的和最后的连续，
            if (_straightLineRanges.Last().End == startIndex)
            {
                // 且方向满足一致，加入直线段
                var vec1 = _points[_straightLineRanges.Last().End] - _points[_straightLineRanges.Last().Start];
                var vec2 = _points[endIndex] - _points[startIndex];
                if (vec1.IsParallelTo(vec2))
                {
                    _straightLineRanges.Last().End = endIndex;
                    return true;
                }

                // 方向不一致但长度满足独立成线
                if (_points[startIndex].DistanceTo(_points[endIndex]) >= CurveFitConfig.MinSegmentLengthTreatAsLine
                    && endIndex - startIndex == 1)
                {
                    _straightLineRanges.Add(new IndexRange(startIndex, endIndex));
                    return true;
                }
            }

            return false;
        }

        private FitResult TryArcFit(int start, int end)
        {
            if (end - start <= 1)
                throw new InvalidOperationException("invalid fit operation");

            var mid = (start + end) / 2;

            CircularArc3d arc = new CircularArc3d(_points[start], _points[mid], _points[end]);
            if (arc.Radius * 2 < _points[start].DistanceTo(_points[end]))
                return new FitResult() { IsArcFit = false, Point1 = _points[start], Point2 = _points[end] };

            double maxDist = -1.0;
            Point3d? pt1 = null;
            Point3d? pt2 = null;

            if (end - start == 2)
            {
                var ptStart = _points[start];
                var ptMid = _points[mid];
                var ptEnd = _points[end];

                var ptMid1 = new Point3d((ptStart.X + ptMid.X) / 2.0,
                    (ptStart.Y + ptMid.Y) / 2.0,
                    (ptStart.Z + ptMid.Z) / 2.0);

                var ptOnCurve = arc.GetClosestPointTo(ptMid1);
                var dist = ptOnCurve.Point.DistanceTo(ptMid1);

                var ptMid2 = new Point3d((ptMid.X + ptEnd.X) /2.0,
                    (ptMid.Y + ptEnd.Y) /2.0,
                    (ptMid.Z + ptEnd.Z) /2.0);
                var ptOnCurve2 = arc.GetClosestPointTo(ptMid2);
                var dist2 = ptOnCurve2.Point.DistanceTo(ptMid2);

                maxDist = dist > dist2 ? dist : dist2;
                pt1 = dist > dist2 ? ptMid1 : ptMid2;
                pt2 = dist > dist2 ? ptOnCurve.Point : ptOnCurve2.Point;
            }
            else
            {
                for (int i = start + 1; i < end; i++)
                {
                    var pt = _points[i];
                    var ptOnCurve = arc.GetClosestPointTo(pt);
                    var dist = ptOnCurve.Point.DistanceTo(pt);
                    if (dist > maxDist)
                    {
                        maxDist = dist;
                        pt1 = pt;
                        pt2 = ptOnCurve.Point;
                    }
                }
            }

            var arcLength = arc.GetLength(arc.GetParameterOf(_points[start]), arc.GetParameterOf(_points[end]), 0.01);

            return new FitResult() { IsArcFit = true, Arc = arc, Point1 = pt1, Point2 = pt2, ArcLength = arcLength, MaxFitTolerance = maxDist };
        }

        private List<Point3d> GetPointsFromPolyline(Polyline2d pl)
        {
            if (pl == null)
                throw new ArgumentNullException(nameof(pl));

            var result = new List<Point3d>();
            var vertexes = pl.GetVertexes();
            for (int i = 0; i < vertexes.Count; i++)
                result.Add(pl.VertexPosition(vertexes[i]));

            return result;
        }

        private List<Point3d> GetPointsFromPolyline(Polyline pl)
        {
            if (pl == null)
                throw new ArgumentNullException(nameof(pl));

            var result = new List<Point3d>();
            for (int i = 0; i < pl.NumberOfVertices; i++)
                result.Add(pl.GetPoint3dAt(i));

            return result;
        }
    }

    internal static class CurveFitConfig
    {
        //public static double MaxCurveRadiusTreatAsLine = 10000.0;// 超过这个长度的圆弧将按照直线处理
        public static double MinSegmentLengthTreatAsLine = 450;// 超过这个长度的分段将按照直线处理
        public static double MaxTol = 3.0;  // 圆弧拟合允许的最大误差值
    }

    internal class FitResult
    {
        public bool IsArcFit = false;
        public CircularArc3d Arc = null;
        public double MaxFitTolerance = 0.0;
        public double ArcLength = 0.0;
        public Point3d? Point1 = null;
        public Point3d? Point2 = null;
    }

    internal class IndexRange
    {
        public IndexRange(int start, int end)
        {
            if (end <= start)
                throw new ArgumentOutOfRangeException();

            this.Start = start;
            this.End = end;
        }

        public CircularArc3d arc = null;

        public int Start;
        public int End;
        public double ArxFitTolerance = -1;
    }
}
